'use client'; /** * [DEPRECATED — 2026-04-26] dpot SignalR 채팅 사이드바 * * Watch 페이지에선 YouTube Live Chat iframe 으로 대체되어 사용 중지. * 코드는 quota 승인 후 재활성화를 위해 보존. * * 재활성화 가이드: ~/.claude/projects/E--workspace-dpot/memory/plan_dpot_chat_reactivate_after_quota.md * * 이 파일을 import 하면 useChat / LeaderboardPanel 도 함께 동작합니다. * 일반적으로는 import 하지 마세요. */ import { useState, useRef, useEffect, useCallback, type CSSProperties, type KeyboardEvent } from 'react'; import useAuth from '@/hooks/useAuth'; import useChat from '@/hooks/useChat'; import { fetchApi } from '@/lib/utils/client'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faUsers, faEye, faEllipsisVertical, faPaperPlane, faTrashCan, faMagnifyingGlassPlus, faMagnifyingGlassMinus, faRotateRight, faClock, faCoins, faCrown, faRankingStar } from '@fortawesome/free-solid-svg-icons'; import type { LiveLeaderboardResponse } from '@/types/response/channel/leaderboard'; import LeaderboardPanel from './LeaderboardPanel'; import './chat-sidebar.scss'; const MIN_FONT_SIZE = 12; const MAX_FONT_SIZE = 22; const DEFAULT_FONT_SIZE = 16; type ChatSidebarProps = { channelSID: string; onDonate?: () => void; }; export default function ChatSidebar({ channelSID, onDonate }: ChatSidebarProps) { const { isAuthenticated } = useAuth(); const { messages, systemMessages, participantCount, participants, sendMessage, clearMessages, refreshChat, requestParticipants, chatConnected } = useChat(channelSID); const [inputValue, setInputValue] = useState(''); const [fontSize, setFontSize] = useState(DEFAULT_FONT_SIZE); const [showMenu, setShowMenu] = useState(false); const [showTime, setShowTime] = useState(false); const messagesRef = useRef(null); const isAutoScrollRef = useRef(true); const [showParticipants, setShowParticipants] = useState(false); const [showLeaderboard, setShowLeaderboard] = useState(false); const [myXp, setMyXp] = useState(null); const menuRef = useRef(null); // 내 XP 주기 갱신 (30초마다) useEffect(() => { if (!isAuthenticated) { return; } let active = true; const fetchMyXp = async () => { try { const res = await fetchApi( `/api/channel/${channelSID}/live-leaderboard?maxRank=1`, { silent: true } ); if (active && res.data) { setMyXp(res.data.myRank?.xp ?? 0); } } catch { // ignore } }; fetchMyXp(); const interval = setInterval(fetchMyXp, 30000); return () => { active = false; clearInterval(interval); }; }, [channelSID, isAuthenticated]); // 메시지 + 시스템 메시지를 시간순 병합 const mergedMessages = (() => { const items: Array< | { type: 'chat'; data: typeof messages[number] } | { type: 'system'; data: typeof systemMessages[number] } > = []; messages.forEach((m) => items.push({ type: 'chat', data: m })); systemMessages.forEach((m) => items.push({ type: 'system', data: m })); items.sort((a, b) => { const timeA = a.type === 'chat' ? a.data.sentAt : a.data.receivedAt; const timeB = b.type === 'chat' ? b.data.sentAt : b.data.receivedAt; return new Date(timeA).getTime() - new Date(timeB).getTime(); }); return items; })(); // 자동 스크롤 useEffect(() => { const el = messagesRef.current; if (!el || !isAutoScrollRef.current) return; el.scrollTop = el.scrollHeight; }, [mergedMessages.length]); // 스크롤 이벤트로 자동 스크롤 제어 const handleScroll = useCallback(() => { const el = messagesRef.current; if (!el) return; const threshold = 50; isAutoScrollRef.current = el.scrollTop + el.clientHeight >= el.scrollHeight - threshold; }, []); // 메뉴 외부 클릭 닫기 useEffect(() => { if (!showMenu) return; const handleClick = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setShowMenu(false); } }; document.addEventListener('mousedown', handleClick); return () => document.removeEventListener('mousedown', handleClick); }, [showMenu]); const handleSend = useCallback(() => { if (!inputValue.trim()) return; sendMessage(inputValue); setInputValue(''); }, [inputValue, sendMessage]); const handleKeyDown = useCallback((e: KeyboardEvent) => { if (e.key === 'Enter' && !e.nativeEvent.isComposing) { e.preventDefault(); handleSend(); } }, [handleSend]); const handleClear = useCallback(() => { clearMessages(); setShowMenu(false); }, [clearMessages]); const handleFontIncrease = useCallback(() => { setFontSize((prev) => Math.min(prev + 2, MAX_FONT_SIZE)); setShowMenu(false); }, []); const handleFontDecrease = useCallback(() => { setFontSize((prev) => Math.max(prev - 2, MIN_FONT_SIZE)); setShowMenu(false); }, []); const handleRefresh = useCallback(() => { setShowMenu(false); refreshChat(); }, [refreshChat]); const handleToggleTime = useCallback(() => { setShowTime((prev) => !prev); setShowMenu(false); }, []); const handleShowParticipants = useCallback(() => { requestParticipants(); setShowParticipants(true); }, [requestParticipants]); const handleCloseParticipants = useCallback(() => { setShowParticipants(false); }, []); const formatTime = (dateStr: string) => { const date = new Date(dateStr); return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`; }; return (
{/* 헤더 */}
{isAuthenticated && myXp !== null && ( )}
{showMenu && (
)}
{/* 메시지 영역 */}
{!chatConnected && (
채팅 서버에 연결 중...
)} {mergedMessages.map((item, index) => { if (item.type === 'system') { return (
{item.data.content}
); } const msg = item.data; const badgeUrl = msg.titleIconUrl || msg.gradeImageUrl || msg.memberIcon; const badgeAlt = msg.titleName ?? ''; const isTopRank = typeof msg.leaderboardRank === 'number' && msg.leaderboardRank >= 1 && msg.leaderboardRank <= 3; return (
{showTime && {formatTime(msg.sentAt)}} {isTopRank && ( )} {badgeUrl && ( {badgeAlt} )} {msg.memberName || msg.memberSID} {msg.content}
); })}
{/* 입력 영역 */}
{isAuthenticated ? (
setInputValue(e.target.value)} onKeyDown={handleKeyDown} maxLength={500} disabled={!chatConnected} /> {onDonate && ( )}
) : (
로그인 후 채팅에 참여하세요
)}
{/* 리더보드 패널 */} setShowLeaderboard(false)} /> {/* 참여자 목록 패널 */} {showParticipants && ( <>

참여자 ({participantCount}명)

    {participants.length === 0 && participantCount === 0 ? (
  • 참여자가 없습니다
  • ) : ( <> {participants.map((p) => (
  • {p.memberName}
  • ))} {participantCount - participants.length > 0 && (
  • 비회원 {participantCount - participants.length}명
  • )} )}
)}
); }